/*
 * Copyright (c) 2022-2023, Arm Limited and Contributors. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cmsis_os2.h"

#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "iotsdk_nimble.h"
#include "nimble/nimble_port.h"
#include "services/gap/ble_svc_gap.h"

#define PW_LOG_MODULE_NAME "main"
#include "pw_log/log.h"

#define VERIFY_OR_EXIT(condition, format, ...) \
    if (!(condition)) {                        \
        PW_LOG_ERROR(format, ##__VA_ARGS__);   \
        abort();                               \
    }

#define INVALID_CONN_HANDLE 0xff

extern ARM_DRIVER_USART *get_example_serial();

static int char_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);

static osEventFlagsId_t event_flags = NULL;

static const char *device_name = "mynewt";

enum {
    APP_EVENT_CONNECTED = 0x1,
    APP_EVENT_DISCONNECTED = 0x2,
    APP_EVENT_ADVERTISING_STOPPED = 0x4,
    APP_EVENT_DATA_WRITTEN = 0x8,
    APP_EVENT_ANY =
        APP_EVENT_CONNECTED | APP_EVENT_DISCONNECTED | APP_EVENT_ADVERTISING_STOPPED | APP_EVENT_DATA_WRITTEN
};

static const char test_pattern[] = {0xde, 0xad, 0xbe, 0xef};

/* 59462f12-9543-9999-12c8-58b459a2712d */
static const ble_uuid128_t example_service_uuid =
    BLE_UUID128_INIT(0x2d, 0x71, 0xa2, 0x59, 0xb4, 0x58, 0xc8, 0x12, 0x99, 0x99, 0x43, 0x95, 0x12, 0x2f, 0x46, 0x59);

/* 5c3a659e-897e-45e1-b016-007107c96df6 */
static const ble_uuid128_t example_char_uuid =
    BLE_UUID128_INIT(0xf6, 0x6d, 0xc9, 0x07, 0x71, 0x00, 0x16, 0xb0, 0xe1, 0x45, 0x7e, 0x89, 0x9e, 0x65, 0x3a, 0x5c);

static void init_logging()
{
    static ARM_DRIVER_USART *serial = NULL;
    if (!serial) {
        serial = get_example_serial();
        if ((serial->Initialize(NULL) != ARM_DRIVER_OK) || (serial->PowerControl(ARM_POWER_FULL) != ARM_DRIVER_OK)
            || (serial->Control(ARM_USART_MODE_ASYNCHRONOUS, 115200) != ARM_DRIVER_OK)) {
            return;
        }
        /* Some drivers have TX and RX enabled by default and lacks option to enable/disable them. */
        int ret = serial->Control(ARM_USART_CONTROL_TX, 1);
        if (ret != ARM_DRIVER_OK && ret != ARM_DRIVER_ERROR_UNSUPPORTED) {
            return;
        }
        ret = serial->Control(ARM_USART_CONTROL_RX, 1);
        if (ret != ARM_DRIVER_OK && ret != ARM_DRIVER_ERROR_UNSUPPORTED) {
            return;
        }
        pw_log_cmsis_driver_init(serial);
    }
}

static const struct ble_gatt_svc_def example_service[] = {
    {
        /*** Service: Security test. */
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = &example_service_uuid.u,
        .characteristics =
            (struct ble_gatt_chr_def[]){
                {
                    /*** Characteristic: test pattern */
                    .uuid = &example_char_uuid.u,
                    .access_cb = char_cb,
                    .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
                },
                {
                    0, /* No more characteristics in this service. */
                },
            },
    },
    {
        0, /* No more services. */
    },
};

static int char_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    (void)conn_handle;
    (void)attr_handle;
    (void)arg;

    if (ble_uuid_cmp(ctxt->chr->uuid, &example_char_uuid.u) == 0) {
        int ret;
        switch (ctxt->op) {
            case BLE_GATT_ACCESS_OP_READ_CHR:
                ret = os_mbuf_append(ctxt->om, &test_pattern, sizeof(test_pattern));
                return ret == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
            case BLE_GATT_ACCESS_OP_WRITE_CHR:
                osEventFlagsSet(event_flags, APP_EVENT_DATA_WRITTEN);
                return 0;
            default:
                return 0;
        }
    }

    return BLE_ATT_ERR_UNLIKELY;
}

static int on_gap_event(struct ble_gap_event *event, void *arg)
{
    (void)arg;
    int flag_res = 0;
    static uint16_t current_conn_handle = INVALID_CONN_HANDLE;

    switch (event->type) {
        case BLE_GAP_EVENT_ADV_COMPLETE:
            flag_res = osEventFlagsSet(event_flags, APP_EVENT_ADVERTISING_STOPPED);
            break;
        case BLE_GAP_EVENT_CONNECT:
            if (event->connect.status == 0) {
                if (current_conn_handle != INVALID_CONN_HANDLE) {
                    return BLE_HS_EREJECT;
                }
                current_conn_handle = event->connect.conn_handle;
                flag_res = osEventFlagsSet(event_flags, APP_EVENT_CONNECTED);
            }
            break;
        case BLE_GAP_EVENT_DISCONNECT:
            current_conn_handle = INVALID_CONN_HANDLE;
            flag_res = osEventFlagsSet(event_flags, APP_EVENT_DISCONNECTED);
            break;
        default:
            break;
    }

    VERIFY_OR_EXIT(((flag_res & osFlagsError) == 0), "osEventFlagsSet failed %d", flag_res)

    return 0;
}

static void advertise(void)
{
    // set advert parameters
    const struct ble_gap_adv_params advert_parameters = {.conn_mode = BLE_GAP_CONN_MODE_UND,
                                                         .disc_mode = BLE_GAP_DISC_MODE_GEN};

    // Fill the advert_fields with advertising data - flags, tx power level, name
    const struct ble_hs_adv_fields advert_fields = {.flags = BLE_HS_ADV_F_DISC_GEN,
                                                    .tx_pwr_lvl_is_present = 1,
                                                    .tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO,
                                                    .name = (uint8_t *)device_name,
                                                    .name_len = strlen(device_name),
                                                    .name_is_complete = 1};

    int error = ble_gap_adv_set_fields(&advert_fields);
    VERIFY_OR_EXIT(!error, "ble_gap_adv_set_fields failed %d", error)

    // Use BLE_OWN_ADDR_RANDOM as we generate a non-resolvable private address (NRPA)
    error = ble_gap_adv_start(BLE_OWN_ADDR_RANDOM, NULL, 100000, &advert_parameters, &on_gap_event, NULL);
    VERIFY_OR_EXIT(!error, "ble_gap_adv_start failed %d", error)
}

static void main_task(void *arg)
{
    PW_LOG_DEBUG("Initialising bluetooth\r\n");

    ble_init();

    // These add our custom services
    int error = ble_gatts_count_cfg(example_service);
    VERIFY_OR_EXIT(!error, "ble_gatts_count_cfg failed %d", error)
    error = ble_gatts_add_svcs(example_service);
    VERIFY_OR_EXIT(!error, "ble_gatts_add_svcs failed %d", error)

    ble_start();

    PW_LOG_INFO("Start advertising\r\n");

    advertise();

    PW_LOG_INFO("Waiting for connection\r\n");

    uint32_t flags = 0;
    while ((flags & osFlagsError) == 0) {
        // we evaluate flags first to avoid checking the flags for error twice
        if (flags & APP_EVENT_CONNECTED) {
            PW_LOG_INFO("Peer connected\r\n");
        }

        if (flags & APP_EVENT_DISCONNECTED) {
            PW_LOG_INFO("Peer disconnected\r\n");
            break;
        }

        if (flags & APP_EVENT_ADVERTISING_STOPPED) {
            PW_LOG_INFO("Advertising stopped\r\n");
            advertise();
        }

        if (flags & APP_EVENT_DATA_WRITTEN) {
            PW_LOG_INFO("Data written\r\n");
        }

        flags = osEventFlagsWait(event_flags, APP_EVENT_ANY, osFlagsWaitAny, osWaitForever);
    }

    VERIFY_OR_EXIT(((flags & osFlagsError) == 0), "osEventFlagsWait failed\r\n");

    PW_LOG_INFO("Demo complete\r\n");
    osThreadExit();
}

int main()
{
    osStatus_t status = osKernelInitialize();
    VERIFY_OR_EXIT((status == osOK), "Failed to initialize the kernel: %d\r\n", status);

    init_logging();

    // We use flags to serialise the events
    event_flags = osEventFlagsNew(NULL);
    VERIFY_OR_EXIT(event_flags, "osEventFlagsNew failed\r\n");

    osThreadAttr_t thread_attr = {.stack_size = 2048};
    osThreadId_t main_thread_id = osThreadNew(main_task, NULL, &thread_attr);
    VERIFY_OR_EXIT(main_thread_id, "osThreadNew failed\r\n");

    status = osKernelStart();
    VERIFY_OR_EXIT((status == osOK), "Failed to start the kernel: %d\r\n", status);

    return EXIT_SUCCESS;
}
